<?php
/*
 * 虽然觉得可能有人写过，不过特么的没找着
 * 
 * 
 * @author hu.qian.1984@gmail.com
 */
class ParseDateFromChinese {
	
	public $dates = array();

	/**
	 * 
	 */
	function __construct() {
		mb_internal_encoding('UTF-8');
		mb_regex_encoding('UTF-8');
		//
	}

	/**
	 * 解析所有日期
	 * @param string $string 
	 */
	function parseAll($string){
		//年
		$this->parseYear($string);
		//月
		$string = $this->stripParsedString($string);
		$this->parseMonth($string);
		//周
		$string = $this->stripParsedString($string);
		$this->parseWeek1($string);
		//星期
		$string = $this->stripParsedString($string);
		$this->parseWeek2($string);
		//天
		$string = $this->stripParsedString($string);
		$this->parseDay1($string);
		//日
		$string = $this->stripParsedString($string);
		$this->parseDay2($string);
		//号
		$string = $this->stripParsedString($string);
		$this->parseDay3($string);
	}
	
	/**
	 * 去掉已被解析的字符串，避免重复
	 */
	function stripParsedString($string){
		static $pos = 0;

		for($pos;$pos<count($this->dates);$pos++){
			$v = $this->dates[$pos];
			if(!empty($v['string']))
				$string = mb_ereg_replace($v['string'], '', $string);
		}
		
		return $string;
	}
	
	/**
	 * 解析出日期时间
	 * 
	 * 中文 时间 定语模式：前面的时间精度大于后面的
	 */
	public function parseDateTime($string){
		//模式判断
		
		//需要预处理
		
		//1、相对模式
		
		//2、绝对模式
		
		//时间
		//12点(时)23分
		mb_ereg_match('.*\d{1,2}点', $string);
		mb_ereg_match('.*\d{1,2}分', $string);
		mb_ereg_match('.*\d{1,2}秒', $string);
		
		//分隔符的时间
		$dateSeperator = '\.\-';
		$timeSeperator = '\:';
		//TODO::附加
		//可以先考虑
		//中午，上午，下午
		
		
		//TODO::
		//2012.05.01
		//3、其他自定义时间
		//午饭时间，下班，睡觉之前
		//世纪。。。
		//2012
	}
	
	/**
	 * 解析时间段
	 * 
	 * 两个时间直接的距离为1~2字符
	 * 到 至 - —— 
	 */
	public function parsePeriod(){
		
	}
	
	/**
	 * (今|去|前|明|后)年 
	 * {2,4}年  90年
	 * \d+年(前|后) 
	 * 
	 * （？xx开始的第x年）无法识别前端标识点，跳过
	 */
	public function parseYear($string, $offset = 0){
		//end
		if($offset > mb_strlen($string)){
			return null;
		}
		$type = 'year';
		$idStr = '年';
		$preEscapeStr = null;
		$preStrArray = array('今','去','前','明','后');
		
		$datetimeStrEndPos = $this->_parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseYear($string, $datetimeStrEndPos + 1);
	}
	
	/**
	 * (这|本|上|下)个?月
	 * \d{1,2}月  3月
	 * \d+月(前|后) 
	 * 
	 * @param type $string
	 * @param type $offset 
	 */
	function parseMonth($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'month';
		$idStr = '月';
		$preEscapeStr = '个';
		$preStrArray = array('这','本','上','下');
		
		$datetimeStrEndPos = $this->_parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseMonth($string, $datetimeStrEndPos + 1);
	}
	
	/**
	 * 
	 * (这|本|上|下)周
	 * \d{1,2}周  周3
	 * \d+周(前|后) 
	 */
	function parseWeek1($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'week';
		$idStr = '周';
		$preEscapeStr = '个';
		$preStrArray = array('这','本','上','下');
		
		$datetimeStrEndPos = $this->_parse2($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseWeek1($string, $datetimeStrEndPos + 1);
	}
	
	/**
	 * (星期)
	 * @param type $string
	 * @param type $offset
	 * @return type 
	 */
	function parseWeek2($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'week';
		$idStr = '星期';
		$preEscapeStr = '个';
		$preStrArray = array('这','上','下');
		
		$datetimeStrEndPos = $this->_parse2($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseWeek2($string, $datetimeStrEndPos + 1);
	}
	
	/**
	 * 天
	 * @param type $string
	 * @param type $offset
	 * @return type 
	 */
	function parseDay1($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'day';
		$idStr = '天';
		$preEscapeStr = '';
		$preStrArray = array('今','昨','前','明','后');
		
		$datetimeStrEndPos = $this->_parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseDay1($string, $datetimeStrEndPos + 1);
	}

	/**
	 * 日
	 * @param type $string
	 * @param type $offset
	 * @return type 
	 */
	function parseDay2($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'day';
		$idStr = '日';
		$preEscapeStr = '';
		$preStrArray = array('今','昨','明');
		
		$datetimeStrEndPos = $this->_parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseDay2($string, $datetimeStrEndPos + 1);
	}

	/**
	 * 号
	 * @param type $string
	 * @param type $offset
	 * @return type 
	 */
	function parseDay3($string, $offset = 0){
		if($offset > mb_strlen($string)){
			return null;
		}
		
		$type = 'day';
		$idStr = '号';
		$preEscapeStr = '';
		$preStrArray = array('');
		
		//TODO::需减少匹配
		$datetimeStrEndPos = $this->_parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray);
		//
		if(is_null($datetimeStrEndPos)){
			return null;
		}
		
		return $this->parseDay3($string, $datetimeStrEndPos + 1);
	}
	
	/**
	 * 一般性
	 */
	function _parse($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray){
		
		//end
		$pos = mb_strpos($string, $idStr, $offset);
		//end
		if($pos === false){
			return null;
		}
		
		$suffixLength = 0;
		//日期时间字符串
		$datetimeString = $idStr;
		
		$i = null;
		//前一个字符，去掉“个”之类
		$pre = mb_substr($string, $pos - 1, 1);
		if(!empty($preEscapeStr) && $pre == $preEscapeStr){
			$datetimeString = $pre.$datetimeString;
			$pre = mb_substr($string, $pos - 2, 1);
			//
		}
		
		if( in_array($pre, $preStrArray) ){
			$datetimeString = $pre.$datetimeString;
			$i = $this->initRelateDate($this->getOffsetByChar($pre),$type, null, null, $datetimeString);
			
			//
		}elseif(is_numeric($pre)){
			
			//数字，从前两位开始
			$numLength = 1;
			while(is_numeric(mb_substr($string, $pos-$numLength-1, 1))){
				$numLength++;
			}
			
			$nums = mb_substr($string, $pos-$numLength, $numLength);
			
			//再前一位
			$temp = mb_substr($string, $pos-$numLength-1, 1);
			//导致识别错误的  第xx年 
			if( in_array($temp, array('第'))){
				//
				
			}
			
			//* \d+年(前|后) 
			$sub = mb_substr($string, $pos + 1, 1);
			//之前，之后。。。
			//11年之后，有歧义，需要注意
			if($sub == '之' ){
				$datetimeString = $datetimeString.$sub;
				$suffixLength += 1;
				$sub = mb_substr($string, $pos + 2, 1);
			}
			
			if( in_array($sub, array('前','后')) ){
				//xx年后半年。。。
				if(mb_substr($string, $pos + 2, 1) == '半'){
					$datetimeString = $nums.$datetimeString.$sub;
					$this->initGregoryDate($nums, $type, null, $datetimeString);
					$suffixLength += 1;
				}else{
					$datetimeString = $nums.$datetimeString.$sub;
					$this->initRelateDate($nums, $type, null,null,$datetimeString);
					$suffixLength += 1;
				}

			// xx年  x月  x号
			}else{
				
				$datetimeString = $nums.$datetimeString;
				$this->initGregoryDate($nums, $type, null, $datetimeString);
			}
			
			
		}else{
			
			//donothing
			;
		}
		
		$SubContentLength = $this->checkSubsequent(mb_substr($string, $pos + $suffixLength + 1), $i, $type);
		
		return $offset = $pos + $suffixLength + $SubContentLength;
	}
	
	
	/**
	 * 星期和周比较特殊，会有缩略用法
	 * 
	 * eg.本周2
	 */
	function _parse2($string, $offset, $type, $idStr, $preEscapeStr, $preStrArray){
		
		//end
		$pos = mb_strpos($string, $idStr, $offset);
		$idStrLen = mb_strlen($idStr);
		//end
		if($pos === false){
			return null;
		}
		
		$suffixLength = 0;
		//日期时间字符串
		$datetimeString = $idStr;
		
		$i = null;
		//前一个字符，去掉“个”之类
		$pre = mb_substr($string, $pos - 1, 1);
		if(!empty($preEscapeStr) && $pre == $preEscapeStr){
			$datetimeString = $pre.$datetimeString;
			$pre = mb_substr($string, $pos - 2, 1);
			//
		}
		
		if( in_array($pre, $preStrArray) ){
			
			$datetimeString = $pre.$datetimeString;
			
			$i = $this->initRelateDate($this->getOffsetByChar($pre),$type, null, null, $datetimeString);
			
			//
		}elseif(is_numeric($pre)){
			
			//数字，从前两位开始
			$numLength = 1;
			while(is_numeric(mb_substr($string, $pos-$numLength-1, 1))){
				$numLength++;
			}
			
			$nums = mb_substr($string, $pos-$numLength, $numLength);
			
			//再前一位
			$temp = mb_substr($string, $pos-$numLength-1, 1);
			//导致识别错误的  第xx年 
			if( in_array($temp, array('第'))){
				//
			}
			
			//* \d+年(前|后) 
			$sub = mb_substr($string, $pos + $idStrLen, 1);
			if( in_array($sub, array('前','后')) ){
				
				$datetimeString = $nums.$datetimeString.$sub;
				$i = $this->initRelateDate($this->getOffsetByChar($pre), $type, null,null,$datetimeString);
				$suffixLength = 1;

			
			}else{
				//donothing
//				$datetimeString = $nums.$datetimeString;
//				$this->initGregoryDate($nums, $type, null, $datetimeString);
			}
			
			
		}else{
			
			//donothing
			;
		}

		//（上）周2
		$sub = mb_substr($string, $pos + $idStrLen, 1);

		if( in_array($sub, array(1,2,3,4,5,6,'日','天')) ){
			$datetimeString .= $sub;
			if(in_array($sub, array('日','天'))){
				$sub = 0;
				if(false){
					$sub = 7;//从周一算一周
				}
			}
			
			//初始
			//周5
			if(is_null($i)){
				//该日与今天的差
				$today = getdate();
				$i = $this->initRelateDate($sub - $today['wday'], 'day', date("Y-m-d"),null,$datetimeString);
			//上 下 这 周5
			}else{
				$relateDate = $this->toStringDate($i);
				//删除之前的
				array_pop($this->dates);
				$i = $this->initRelateDate($sub, 'day', $relateDate,null,$datetimeString);
			}
			
		//默认
		}
		
		$SubContentLength = $this->checkSubsequent(mb_substr($string, $pos + $suffixLength + 1), $i, $type);
		
		return $offset = $pos + $suffixLength + $SubContentLength;
	}
	//
			
	/**
	 * 检查到一个日期特征字符之后解析接下来的
	 * 
	 * @param string $type 调用处的 时间类型，此处为相应的之后的时间
	 */
	private function checkSubsequent($string, $i = null, $type='year'){
		if(is_null($i)){
			$i = count($this->dates) - 1;
		}
		
		$SubContentLength = 0;
		
		switch ($type){
			case 'year':case 'month':case 'week':case 'day':
				$hasSubContent = mb_ereg('^的?第?(\d+)个?(月|周|星期|日|天|号)+?', $string, $matches);
				
				//检查下一个
				if($hasSubContent){
					$subType = $this->getTypeFromChar($matches[2]);
					
					if(!$this->isSubType($type, $subType)){
						//
						return 0;
					}
					
					$num = $matches[1] -1;
					$this->addRelateDate($i, $num, $subType, $this->toStringDate($i), $matches[0]);
					
					
					$SubContentLength = mb_strlen($matches[0]);
					$string = mb_substr($string, $SubContentLength);
					
					$this->checkSubsequent($string, $i, $subType);
				
				//x月第3周周2，
				
				}elseif($type == 'day'){
					
					//TODO::x月第3个周2 
					$hasSubContent = mb_ereg('^的?(周|星期)(\d|日|天)+?', $string, $matches);
					
					//检查下一个
					if($hasSubContent){
						$subType = $this->getTypeFromChar($matches[1]);
						//2012年3月27日星期2，匹配为：2012年3月27日 和 星期2
						if(!$this->isSubType($type, $subType)){
							//
							return 0;
						}
						
						$wday = $matches[2];
						if(in_array($wday, array('日','天'))){
							$wday = 0;
							if(false){
								$wday = 7;//从周一算一周
							}
						}
						
						$num = $wday;
						$this->addRelateDate($i, $num, $subType, $this->toStringDate($i), $matches[0]);


						$SubContentLength = mb_strlen($matches[0]);
						$string = mb_substr($string, $SubContentLength);
						$this->checkSubsequent($string, $i, $subType);
					}
					
				}
				
				break;
				
			default :
				//
				;
		}
		
		return $SubContentLength;
	}

	/**
	 * 使用相对日期初始
	 * @param type $num
	 * @param type $type
	 * @param type $relateDate 
	 */
	public function initRelateDate($num, $type = 'year', $relateDate = null, $at = null, $string = ''){
		$this->dates[] = array();
		$i = count($this->dates) - 1;
		
		$this->addRelateDate($i, $num, $type, $relateDate);
		
//		$this->addGregoryDate($i, $num, $type);
		//位于字符的起始位置，若只有一个则无意义
		if(is_numeric($at)){
			$this->dates[$i]['at'] = $at;
		}
		if($string){
			$this->dates[$i]['string'] = $string;
		}
		
		return $i;
	}
	
	/**
	 * 使用相对日期细化
	 * @param type $i
	 * @param type $num
	 * @param type $type
	 * @param type $relateDate 
	 * @todo 增加相对日期
	 */
	public function addRelateDate($i, $num, $type = 'year', $relateDate = null, $string = ''){
		//针对 2周前
		if($type == 'week'){
			$num *= 7;
			$today = getdate();

			$num -= $today['wday'];

			$type = 'day';
		}

		$numSign = ($num>0)?'+':'';

		if(!is_null($relateDate)){
			$time = strtotime("{$numSign}{$num} $type", strtotime($relateDate));
		}else{
			$time = strtotime("{$numSign}{$num} $type");
		}

		switch ($type){
			case 'second':
				!isset ($this->dates[$i]['second']) 
					&& $this->dates[$i]['second']	= date('s', $time);
			case 'minute':
				!isset ($this->dates[$i]['minute']) 
					&& $this->dates[$i]['minute']	= date('i', $time);
			case 'hour':
				!isset ($this->dates[$i]['hour']) 
					&& $this->dates[$i]['hour']	= date('H', $time);
			case 'day':
				!isset ($this->dates[$i]['day']) 
					&& $this->dates[$i]['day']	= date('d', $time);
			case 'month':
				!isset ($this->dates[$i]['month']) 
					&& $this->dates[$i]['month']	= date('m', $time);
			case 'year':
				!isset ($this->dates[$i]['year']) 
					&& $this->dates[$i]['year']	= date('Y', $time);
		}
		
		
		if($string){
			$this->dates[$i]['string'] .= $string;
		}
	}
	
	/**
	 * 使用格里高利历初始
	 * @param type $num
	 * @param type $type 
	 */
	public function initGregoryDate($num, $type = 'year', $at = null, $string=''){
		$this->dates[] = array();
		$i = count($this->dates) - 1;
		
		$this->addGregoryDate($i, $num, $type);

		//补充默认值，当天
		if($type !== 'year'){
			$this->addRelateDate($i, 0, $type);
		}

		//位于字符的起始位置，若只有一个则无意义
		if(is_numeric($at)){
			$this->dates[$i]['at'] = $at;
		}
		if($string){
			$this->dates[$i]['string'] = $string;
		}
		
		return $i;
	}
	
	/**
	 * 使用格里高利历细化日期
	 * @param type $pos
	 * @param string $num
	 * @param type $type 
	 */
	public function addGregoryDate($pos, $num, $type = 'year'){
		switch ($type){
			case 'second':
			case 'minute':
			case 'hour':
			case 'day':
			case 'month':
				$num = str_pad($num, 2, '0', STR_PAD_LEFT);
				!isset ($this->dates[$pos][$type]) 
					&& $this->dates[$pos][$type] = $num;
				break;
			case 'year':
				//2位数年份补全
				if(strlen($num) == 2){
					if(intval($num) >= 70){
						$num = '19'.$num;
					}else{
						$num = '20'.$num;
					}
				}
				!isset ($this->dates[$pos][$type]) 
					&& $this->dates[$pos][$type] = $num;
				break;
			default :
				;
		}
	}
	
	/**
	 * 
	 */
	public function getOffsetByChar($char){
		$offset = null;
		switch ($char){
			case '今':case '这':case '本':
				$offset = 0;
				break;
			case '去':case '上':case '昨':
				$offset = -1;
				break;
			case '前':
				$offset = -2;
				break;
			case '明':case '下':
				$offset = 1;
				break;
			case '后':
				$offset = 2;
				break;
			//为定义的情况
			default :
				throw new Exception('Undefined Char'.$char);
		}
		
		
		return $offset;
	}
	
	public function getTypeFromChar($char){
		$type = '';
		switch ($char){
			case '年':
				$type = 'year';
				break;
			case '月':
				$type = 'month';
				break;
			case '周':
				$type = 'week';
				break;
			case '日':case '天':case '号':
				$type = 'day';
				break;
			case '时':case '点':
				$type = 'hour';
				break;
			case '分':
				$type = 'minute';
				break;
			case '秒':
				$type = 'second';
				break;
		}
		
		return $type;
	}
	
	
	public function toStringDate($pos = 0){
		$date = $this->dates[$pos];
		$year = empty ($date['year'])?date('Y',0):$date['year'];
		$month = empty ($date['month'])?date('m',0):$date['month'];
		$day = empty ($date['day'])?date('d',0):$date['day'];
		$hour = empty ($date['hour'])?date('H',0):$date['hour'];
		$minute = empty ($date['minute'])?date('i',0):$date['minute'];
		$second = empty ($date['second'])?date('s',0):$date['second'];
		
		return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}";
	}
	
	/**
	 * 本类型的下一个
	 * @param type $type 
	 */
	public function getSubType($type){
		
		$types = array(
			'year','month','day','hour','minute','second'
		);
		
		$pos = array_search($type, $types);
		if($pos >= (count($types)-1))
			return null;
		
		return $types[$pos+1];
	}
	
	/**
	 * @param type $type 
	 */
	public function isSubType($type, $subType){
		$types = array(
			'year','month','day','hour','minute','second'
		);
		
		$pos = array_search($type, $types);
		$subTypePos = array_search($subType, $types);		
		
		return $pos < $subTypePos;
	}
	
}